Repository navigation
Conversation
…dentifier The scan that finds the end of an identifier for a diagnostic existed twice. #31783 fixed three steps in `bun_ast::range_of_identifier`: a name that starts with a `\u{...}` escape, the byte after the closing brace of an escape, and a name that ends the file. The copy in lexer.rs did not get those fixes, and 45 call sites still called it. Delete the copy. 44 call sites in the parser, the resolver and the linker call `Source::range_of_identifier` with the same arguments. The `import.meta` site stores the length of the `import` keyword and does not scan.
|
Status
|
|
No actionable comments were generated in the recent review. WalkthroughParser, linker, and resolver diagnostic locations now use ChangesIdentifier diagnostic ranges
Suggested reviewers: Merge Risk:
|
There was a problem hiding this comment.
LGTM — a mechanical dedup that routes every identifier-range diagnostic through the already-fixed Source::range_of_identifier, with tests that pin each corrected case.
What was reviewed:
- All 44 swapped call sites pass the same source/loc as before; no caller of the deleted lexer copy remains, and the leftover
js_lexer/CodepointIteratorimports are still live. - The hard-coded
len: 6forimport.meta: both callers ofparse_import_exprpass the loc of aT::TImporttoken, and an escapedimportlexes asT::TEscapedKeyword, so the token is always the 6 bytesimport. - The surviving body in
src/ast/lib.rsis unchanged here, already had four callers and unit tests; the leading-\u{scan is bounded by source length and only runs on diagnostic paths. - New tests assert exact message plus covered-text pairs via
toEqual, and no existing test asserted the three old truncated message strings.
Optional nit: the doc comment above range_of_identifier in src/ast/lib.rs:1089 ("Was bun_js_parser::lexer::rangeOfIdentifier. Moved into logger to break logger→js_parser") now describes a copy that no longer exists; worth trimming while you're here.
Extended reasoning...
The PR deletes the duplicate range_of_identifier in src/js_parser/lexer.rs and rewrites 44 call sites across the parser, linker and resolver to call the pre-existing Source::range_of_identifier method, plus one site that replaces a scan of the import keyword with a fixed 6-byte range. It touches no security-sensitive surface; the only effect is the length of diagnostic ranges (error.position.length) and three message texts that were previously truncated. Approval was decided by the fact that every swap is argument-identical, the single semantic change was verified against both callers and the lexer's escaped-keyword handling, the surviving helper is untouched and already tested, and the added test.each suites assert exact values and cover leading/trailing escapes, EOF and non-ASCII. No CODEOWNERS entry applies and the timeline shows no outstanding reviewer objections.
|
Thanks for the review. I left the doc comment at
|
Problem
src/ast/lib.rs:1093, not the copy atsrc/js_parser/lexer.rs:3351, which 45 of 49 call sites use.const xat the end of a file reportsposition.length0.break \u{79}covers\u.export { \u{69}f }reportsExpected identifier but found "\u".Fix
Source::range_of_identifierwith the same arguments.import.metasite stores the length ofimport. No scan logic is new:src/ast/lib.rsequals main.test/bundler/transpiler/transpiler.test.js,test/bundler/bun-build-api.test.ts(48 new rows, main fails 42). Alsobundler_edgecase.test.ts,esbuild/importstar.test.ts.letas a name that a let, const or using declaration binds in a pattern or with an escape #48043, Refuse a macro import that is used outside of a call #46239, bundler: warn about direct eval in ES modules, with one guard for weird-code warnings #46890.Background
error.position.length.lexer.rsand a port of the fixes. The forwarder keeps a second public name. The port keeps two bodies.Downsides
eval: 131 to 138, gdb, release builds).\u{...}is scanned to its closing brace. A 1,000-byte escape costs 12,086 instructions, up from 104.Notes
The three steps the copy had wrong
const x(end of file)xfor (;;) break abc(end of file)ababcfor (;;) { break \u{79}; }\u\u{79}for (;;) { break a\u{62}; }a\u{62};a\u{62}export { a\u{62} as x };a\u{62} asa\u{62}for (;;) { break a\u{62}é; }éa\u{62}é;after the name, main is right.\u{...}there. The loop then stopped at{.loc.start < -1. The surviving body returnsRange::NONE.src/js_lexer.zig:3104) has the same three steps. When Resolve the audited TODO(port)/TODO(b2)/PORT NOTE backlog #31783 fixed the other copy, 41 call sites called this one. Now 45 do.What a user sees
error.position.lengthof aBuildMessage(Bun.Transpiler,Bun.buildlogs, a module that fails to load)src/runtime/bake/client/overlay.ts:725)src/aot/lower/transpiled.rs:105)^at the start of the range and does not read the length. Terminal output changes only in three message texts:export { \u{69}f };:Expected identifier but found "\u"is nowExpected identifier but found "\u{69}f".export { x, typeofat the end of a file:Expected identifier but found "typeo"is now"typeof".export default interfaceat the end of a.jsfile:Unexpected "interfac"is nowUnexpected "interface".esbuild
RangeOfIdentifierin esbuild (internal/js_lexer/js_lexer.go:631) gives the same ranges as the surviving body for the two escape steps. For an identifier that ends the file it returns length 0, because it falls through toRangeOfString. Bun covers the identifier. The unit testidentifier_at_eof_includes_last_codepointfrom #31783 pins that.How to replay the edit
git grep -l 'lexer::range_of_identifier\|lex::range_of_identifier' -- srclists, replaceX::range_of_identifier(SRC, LOC)withSRC.range_of_identifier(LOC). Two calls span lines (p.rs:3659,parse/parse_property.rs:153). One call inlexer.rs:1875has no path prefix.parse/parse_import_export.rs:21by hand.parse_import_exprhas two callers. Both pass the loc of aT::TImporttoken. An escaped keyword lexes asT::TEscapedKeyword, so that token is always the bytesimport.lexer.rs:3351-3409. Remove the five imports that the compiler then names (LinkerContext.rs,parse_import_export.rs,scan_symbols.rs,visit/mod.rs,package_json.rs). Runcargo fmt --all.Tests
bun-build-api.test.tsbecause only the JS API exposes a length.src/ast/lib.rs. That run used an earlier set of 39 parser rows and 4 linker rows. Without the leading-escape skip, 20 and 2 fail. Withoutcursor.width = 0, 18 and 2. Without the end-of-file width, 7 and 0.bun bd test test/bundler/transpiler/transpiler.test.js: 300 pass, 0 fail.bun bd test test/bundler/bun-build-api.test.ts: 96 pass and 1 timeout. The timeout is the existing stress testBun.build can be called thousands of times in one process without crashing. It needs 400 builds in 180 s, and the ASAN debug build here takes about 1 s for each. The release builds of main and of this branch both run 40 of those builds in 1.3 s.bundler_edgecase.test.ts,esbuild/importstar.test.ts,parse-error-column.test.ts,resolve-error.test.ts,assign-to-import.test.ts,decorators.test.ts,scope-mismatch-panic.test.ts, and the regression tests 23649 and 012039.Measurements (release builds of main 7ca0042 and of this branch,
--profile=release)function eval(){}: 131 to 138var arguments = 1: 226 to 238require(x): 188 to 198import(x): 169 to 178import.meta: 1 scan of 169 instructions to 0 scans. A file of 1,000import.metastatements hits the scan 1,000 times on main and 0 times now..text: 110,627,497 to 110,625,705 bytes (llvm-size).bun_js_parser::lexer::range_of_identifier(1,737 bytes) is gone.<bun_ast::Source>::range_of_identifieris 2,042 bytes in both.objdump): no allocator call. The targets arepanic_bounds_check,unwrap_failed, andmemcpyon the non-ASCII path.Still wrong after this PR (same output on main and on this branch, lines as on main)
visit/visit_expr.rs:994,:1011,:1027,:1043,visit/visit_binary.rs:743).this.#\u{62}covers#\. In the same matrix 2,805 of 4,095 rows are wrong.parse/parse_prefix.rs:400also starts at the object.delete this.#bcdefcoversthis.#.export { a as "x", b as "x" }and a second"__proto__"key.export { "a b" }reportsExpected identifier but found "". esbuild falls back toRangeOfString.continueto a label that is not a loop underlinescontinue, not the label (visit/visit_stmt.rs:1403).parse/parse_property.rs:153).import { type as eval }reportsCannot use eval as an identifier herewith length 0 (parse/parse_import_export.rs:187).import { x as eval }underlinesx, noteval(parse/parse_import_export.rs:230).awaiterror underlines the token afterawait(lexer.rs:1884, Irrelevant error: using await in non-async function #13097).hoist_symbolswrites its own redeclaration error, with no quotes and with the error on the first declaration (p.rs:3643).export default 1; export default function foo() {}underlinesfoo(parse/parse_stmt.rs:1158).import abc = require('x')stores no loc. A note aboutabcsits at offset 0 (parse/parse_stmt.rs:1635).Open PRs that call the deleted name
letas a name that a let, const or using declaration binds in a pattern or with an escape #48043, Refuse a macro import that is used outside of a call #46239 and bundler: warn about direct eval in ES modules, with one guard for weird-code warnings #46890 merge onto this branch with no text conflict and then do not compile. Each added call needsself.source.range_of_identifier(loc). The method is public on main today, so they can change before this lands.src/resolver/package_json.rs.usingdeclarations may appear (switch clauses, for heads,awaitnewline) #40813, js_parser: report missing await, yield and binding pattern early errors #40821, parser: report strict-mode early errors and accept legacy sloppy syntax #40827, js_parser: keep "use strict" and the directive prologue in the output #40838, js_parser: make the ESM/CJS classification and the module/exports bindings agree #40840, js_parser: report delete of a bare identifier in strict mode #40861, js_parser: reject a modifier or an escape in front of a class static block #44909, bundler: keep the CommonJS wrapper of a package that a file writes to through require() #46216, Implement dynamicimport.defer()(TC39 Stage 3) #31244, node: ESM resolver error-code parity — ERR_PACKAGE_PATH_NOT_EXPORTED and friends (+4 tests) #35711 and bundler: remove unsafe from bundle_v2, ParseTask, LinkerContext and the bundler thread pool #40438 also add or change a call. They already conflict with main.no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/bun-build-api.test.ts